import copy
import datetime
import importlib
import io
import json
import os
import shutil
import sys
import tempfile
import types
from builtins import KeyError, len

import requests
import yaml
from bs4 import BeautifulSoup
from django.db.models import Q
from httprunner.exceptions import VariableNotFound, ParamsError
from requests.cookies import RequestsCookieJar

from FasterRunner.settings import BASE_DIR, logger
from fastrunner import models
from fastrunner.utils.hruner_v3 import gen_hrun_v3_args, HrunV3, hrun_v3_summary_adapt
from fastrunner.utils.parser import Format

TEST_NOT_EXISTS = {
    "code": "0102",
    "status": False,
    "msg": "节点下没有接口或者用例集"
}

TEST_BACK_ASYNC = {
    "code": "1001",
    "status": "true",
    "msg": "用例运行中，请稍后查看报告"
}

def is_function(tup):
    """ Takes (name, object) tuple, returns True if it is a function.
    """
    name, item = tup
    return isinstance(item, types.FunctionType)


def is_classobj(tup):
    """Takes (name, object) tuple, returns True if it is a class.
    """
    name, item = tup
    if type(item).__name__ == 'type':
        return True
    return False


def is_variable(tup):
    """ Takes (name, object) tuple, returns True if it is a variable.
    """
    name, item = tup
    if callable(item):
        # function or class
        return False

    if isinstance(item, types.ModuleType):
        # imported module
        return False

    if name.startswith("_"):
        # private property
        return False

    return True


class FileLoader(object):

    @staticmethod
    def dump_yaml_file(yaml_file, data):
        """ dump yaml file
        """
        with io.open(yaml_file, 'w', encoding='utf-8') as stream:
            yaml.dump(data, stream, indent=4, default_flow_style=False, encoding='utf-8', allow_unicode=True)

    @staticmethod
    def dump_json_file(json_file, data):
        """ dump json file
        """
        with io.open(json_file, 'w', encoding='utf-8') as stream:
            json.dump(data, stream, indent=4, separators=(',', ': '), ensure_ascii=False)

    @staticmethod
    def dump_python_file(python_file, data):
        """dump python file
        """

        with io.open(python_file, 'w', encoding='utf-8') as stream:
            stream.write(data)
            stream.flush()

    @staticmethod
    def dump_binary_file(binary_file, data):
        """dump file
        """
        with io.open(binary_file, 'wb') as stream:
            stream.write(data)

    @staticmethod
    def copy_file(path, to_path):
        """
        copy file to_path
        """
        shutil.copyfile(path, to_path)

    @staticmethod
    def load_python_module(file_path):
        """ load python module.

        Args:
            file_path: python path

        Returns:
            dict: variables and functions mapping for specified python module

                {
                    "variables": {},
                    "functions": {}
                }

        """
        logger.info("invoke load_python_module:[file_path={}]".format(file_path))
        debugtalk_module = {
            "variables": {},
            "functions": {}
        }
        sys.path.insert(0, file_path)
        try:
            for root, dirs, files in os.walk(file_path):
                for name in files:
                    if name.endswith(".py"):
                        # 遍历临时目录，将py模块导入系统
                        module = importlib.import_module(name.rstrip(".py"))
                        importlib.reload(module)
                        # logger.info("reload 模块：{}".format(name))
                        for name, item in vars(module).items():
                            if is_function((name, item)):
                                debugtalk_module["functions"][name] = item
                            elif is_variable((name, item)):
                                if isinstance(item, tuple):
                                    continue
                                # 强制转换成字符串，避免在驱动代码中定义的变量为对象类型时，在输出报告in_out 中解析json报错。
                                debugtalk_module["variables"][name] = str(item)
                            else:
                                pass
        finally:
            sys.path.pop(0)
        logger.info("invoke load_python_module end:[debugtalk_module={}]".format(debugtalk_module))
        return debugtalk_module


def parse_tests(testcases, debugtalk, project, name=None, config=None):
    """get test case structure
        testcases: list
        config: none or dict
        debugtalk: dict
    """
    logger.info("invoke parse_tests:[testcases={}, debugtalk={}, project={}, name={}, config={}]".format(testcases, debugtalk, project, name, config))
    refs = {
        "env": {},
        "def-api": {},
        "def-testcase": {},
        "debugtalk": debugtalk
    }
    testset = {
        "config": {
            "name": testcases[-1]["name"],
            "variables": []
        },
        "teststeps": testcases,
    }

    if config:
        if "parameters" in config.keys():
            for content in config["parameters"]:
                for key, value in content.items():
                    try:
                        content[key] = eval(value.replace("\n", ""))
                    except:
                        content[key] = value
        if 'outParams' in config.keys():
            config["output"] = []
            out_params = config.pop('outParams')
            for params in out_params:
                config["output"].append(params["key"])
        testset["config"] = config

    if name:
        testset["config"]["name"] = name

    global_variables = []

    for variables in models.Variables.objects.filter(project__id=project).values("key", "value"):
        if testset["config"].get("variables"):
            for content in testset["config"]["variables"]:
                if variables["key"] not in content.keys():
                    global_variables.append({variables["key"]: variables["value"]})
        else:
            global_variables.append({variables["key"]: variables["value"]})

    if not testset["config"].get("variables"):
        testset["config"]["variables"] = global_variables
    else:
        testset["config"]["variables"].extend(global_variables)

    testset["config"]["refs"] = refs
    logger.info("invoke parse_tests end:[testset={}]".format(testset))
    return testset


def empty_file_path(filepath):
    """ 清空指定文件夹下，12小时前的文件
    :param filepath:
    :return:
    """
    now = datetime.datetime.now()
    deltaH = datetime.timedelta(seconds=43200)
    if os.path.exists(filepath):
        del_list = os.listdir(filepath)
        for f in del_list:
            file_path = os.path.join(filepath, f)
            f = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
            if f < (now - deltaH):
                if os.path.isfile(file_path):
                    logger.debug(f"删除临时文件:{file_path}")
                    os.remove(file_path)
                elif os.path.isdir(file_path):
                    logger.debug(f"删除临时文件夹:{file_path}")
                    shutil.rmtree(file_path)
    else:
        os.makedirs(filepath)


@logger.catch
def load_debugtalk(project):
    """import debugtalk.py in sys.path and reload
        project: int
    """
    logger.info("invoke load_debugtalk:[project={project}]".format(project=project))
    empty_file_path(os.path.join(BASE_DIR, 'tempWorkDir'))
    tempfile_path = tempfile.mkdtemp(prefix='tempHttpRunner', dir=os.path.join(BASE_DIR, 'tempWorkDir'))
    logger.debug("mkdtemp sucess:%s" % tempfile_path)
    debugtalk_path = os.path.join(tempfile_path, 'debugtalk.py')
    try:
        # 控制导入顺序，先导入project_id=0 即平台驱动，然后再导入本项目驱动。解决问题：函数，变量同名时，能够以项目驱动为优先。
        # 导入平台驱动
        py_files_pt = models.Pycode.objects.filter(Q(project__id=0))
        tempfile_dir = os.path.join(tempfile_path, "pt")
        for file in py_files_pt:
            if not os.path.exists(tempfile_dir):
                os.makedirs(tempfile_dir)
            file_path = os.path.join(tempfile_dir, file.name)
            FileLoader.dump_python_file(file_path, file.code)
        debugtalk = FileLoader.load_python_module(tempfile_dir)
        # 导入项目驱动
        py_files_pj = models.Pycode.objects.filter(Q(project__id=project))
        tempfile_dir = os.path.join(tempfile_path, "pj")
        for file in py_files_pj:
            if not os.path.exists(tempfile_dir):
                os.makedirs(tempfile_dir)
            file_path = os.path.join(tempfile_dir, file.name)
            FileLoader.dump_python_file(file_path, file.code)
        pj_debugtalk = FileLoader.load_python_module(tempfile_dir)
        # 以项目驱动为优先
        debugtalk['variables'].update(pj_debugtalk['variables'])
        debugtalk['functions'].update(pj_debugtalk['functions'])

        # 同步测试文件到临时目录
        testdata_files = models.ModelWithFileField.objects.filter(project__id=project)
        for testdata in testdata_files:
            testdata_path = os.path.join(tempfile_path, testdata.name)
            myfile_path = os.path.join(BASE_DIR, 'media', str(testdata.file))
            FileLoader.copy_file(myfile_path, testdata_path)
        logger.info("invoke load_debugtalk end:[debugtalk_content={debugtalk_content},debugtalk_path={debugtalk_path}]"
                    .format(debugtalk_content=debugtalk, debugtalk_path=debugtalk_path))
        return debugtalk, debugtalk_path
    except Exception as e:
        logger.error(str(e))
        # os.chdir(BASE_DIR)
        # shutil.rmtree(os.path.dirname(debugtalk_path))
        raise SyntaxError(str(e))


@logger.catch
def debug_suite(suite, project, obj, config, save=True):
    """debug suite
           suite :list
           pk: int
           project: int
    """
    logger.info("invoke debug_suite:[suite={}, project={}, obj={}, config={}, save={}]".format(suite, project, obj, config, save))
    if len(suite) == 0:
        return TEST_NOT_EXISTS

    test_sets = []
    debugtalk = load_debugtalk(project)
    debugtalk_content = debugtalk[0]
    debugtalk_path = debugtalk[1]
    # os.chdir(os.path.dirname(debugtalk_path))

    try:
        for index in range(len(suite)):
            testcases = copy.deepcopy(
                parse_tests(suite[index], debugtalk_content, project, name=obj[index]['name'], config=config[index]))
            test_sets.append(testcases)

        kwargs = {
            "failfast": False
        }
        logger.info("invoke HttpRunner")

        init_list = gen_hrun_v3_args(test_sets)
        summary = []
        for init in init_list:
            suit_summary = HrunV3(init[0], init[1], debugtalk_content).test_start().get_summary().dict()
            logger.info(f"httprunner-v3 suit summary:{suit_summary}")
            summary.append(suit_summary)
        summary = hrun_v3_summary_adapt(summary)
        if save:
            save_summary("", summary, project, type=1)
        logger.info(f"invoke debug_suite end:adapt summary:{summary}")
        return summary
    except Exception as e:
        logger.error(str(e))
        raise SyntaxError(str(e))
    finally:
        # if os.path.exists(os.path.dirname(debugtalk_path)):
        #     shutil.rmtree(os.path.dirname(debugtalk_path))
        os.chdir(BASE_DIR)


@logger.catch
def debug_api(testcase, project, name=None, config=None, save=True, test_data=None, report_name=''):
    """debug api
        testcase :dict or list
        project: int
    """
    logger.info(f"invoke debug_api:[testcase={testcase},project={project},name={name}, config={config}, save={save}, test_data={test_data},report_name={report_name}]")

    if len(testcase) == 0:
        return TEST_NOT_EXISTS
    # testcases
    if isinstance(testcase, dict):
        """
        httprunner scripts or teststeps
        """
        testcase = [testcase]

    debugtalk_content, debugtalk_path = load_debugtalk(project)

    try:
        testcase_list = [parse_tests(testcase, debugtalk_content, project, name=name, config=config)]

        fail_fast = False
        if config and 'failFast' in config.keys():
            fail_fast = True if (config["failFast"] == 'true' or config["failFast"] is True) else False

        kwargs = {
            "failfast": fail_fast
        }
        if test_data is not None:
            os.environ["excelName"] = test_data[0]
            os.environ["excelsheet"] = test_data[1]
        logger.info("invoke HttpRunner")
        init_list = gen_hrun_v3_args(testcase_list)
        summary = []
        for init in init_list:
            hrun = HrunV3(init[0], init[1], debugtalk_content).test_start()
            suit_summary = hrun.get_summary().dict()
            logger.info(f"httprunner-v3 suit summary:{suit_summary}")
            summary.append(suit_summary)
        summary = hrun_v3_summary_adapt(summary)
        logger.info(f"invoke debug_suite end:adapt summary:{summary}")
        if save:
            save_summary(report_name, summary, project, type=1)
        return summary
    except VariableNotFound as e:
        logger.error(str(e))
        return {"status": False, "msg": "VariableNotFound:" + str(e)}
    except ParamsError as e:
        logger.error(str(e))
        return {"status": False, "msg": "ParamsError:" + str(e)}
    except Exception as e:
        logger.error(str(e))
        return {"status": False, "msg": str(e)}

    finally:
        logger.info("invoke debug_api end")


def load_test(test, project=None):
    """
    format testcase
    """
    logger.info("invoke load_test:[test={},project={}]".format(test, project))
    try:
        format_http = Format(test['newBody'])
        format_http.parse()
        testcase = format_http.testcase

    except KeyError:
        if 'case' in test.keys():
            if test["body"]["method"] == "config":
                case_step = models.Config.objects.get(name=test["body"]["name"], project=project)
            else:
                case_step = models.CaseStep.objects.get(id=test['id'])
        else:
            if test["body"]["method"] == "config":
                case_step = models.Config.objects.get(name=test["body"]["name"], project=project)
            else:
                case_step = models.API.objects.get(id=test['id'])

        testcase = eval(case_step.body)
        name = test['body']['name']

        if case_step.name != name:
            testcase['name'] = name
    logger.info("invoke load_test end:[testcase={}]".format(testcase))
    return testcase


def parse_summary(summary):
    """序列化summary
    """
    for detail in summary["details"]:

        for record in detail["records"]:

            for key, value in record["meta_data"]["request"].items():
                if isinstance(value, bytes):
                    record["meta_data"]["request"][key] = value.decode("utf-8")
                if isinstance(value, RequestsCookieJar):
                    record["meta_data"]["request"][key] = requests.utils.dict_from_cookiejar(value)

            for key, value in record["meta_data"]["response"].items():
                if isinstance(value, bytes):
                    record["meta_data"]["response"][key] = value.decode("utf-8")
                if isinstance(value, RequestsCookieJar):
                    record["meta_data"]["response"][key] = requests.utils.dict_from_cookiejar(value)

            if "text/html" in record["meta_data"]["response"]["content_type"]:
                record["meta_data"]["response"]["content"] = \
                    BeautifulSoup(record["meta_data"]["response"]["content"], features="html.parser").prettify()

    return summary


def save_summary(name, summary, project, type=2):
    """保存报告信息
    """
    logger.info("invoke save_summary:[name={},summary={},project={},type={}]".format(name, summary, project, type))
    if "status" in summary.keys():
        return
    if name == "":
        name = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    simple_summary = {
        "time": summary["time"],
        "platform": summary["platform"],
        "stat": summary["stat"],
        "success": summary["success"],
    }
    project_class = models.Project.objects.get(id=project)
    report = models.Report.objects.create(**{
        "project": project_class,
        "name": name,
        "type": type,
        "summary": json.dumps(simple_summary, ensure_ascii=False)
    })
    report.save()
    report_detail = models.ReportDetail.objects.create(**{
        "project": project_class,
        "name": name,
        "report": report,
        "summary": json.dumps(summary, ensure_ascii=False)
    })
    report_detail.save()
    logger.info("invoke save_summary end:")
